package com.godyao.service.impl;

import com.godyao.enums.OrderStatusEnum;
import com.godyao.enums.ResponseCodeEnum;
import com.godyao.enums.ResultMsg;
import com.godyao.model.*;
import com.godyao.pojo.SecKillMessage;
import com.godyao.repository.GoodsRepository;
import com.godyao.repository.OrderRepository;
import com.godyao.repository.SecKillGoodsRepository;
import com.godyao.repository.SecKillOrderRepository;
import com.godyao.service.RabbitMqSecKillSender;
import com.godyao.service.SecKillService;
import lombok.extern.slf4j.Slf4j;
import org.apache.catalina.security.SecurityUtil;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.Instant;
import java.util.*;
import java.util.concurrent.TimeUnit;

/**
 * @author godyao
 * @date 2022/3/17
 */
@Slf4j
@Service
public class SecKillServiceImpl implements SecKillService, InitializingBean {
    @Autowired
    private SecKillGoodsRepository secKillGoodsRepository;
    @Autowired
    private SecKillOrderRepository secKillOrderRepository;
    @Autowired
    private GoodsRepository goodsRepository;
    @Autowired
    private OrderRepository orderRepository;
    @Autowired
    private RedisTemplate redisTemplate;
    @Autowired
    private RabbitMqSecKillSender rabbitMqSecKillSender;
    @Autowired
    private RedisScript unlockScript;

    /**
     * 内存标记 减少对redis的访问
     */
    private static final Map<Long, Boolean> GOODS_CAN_BY_MAP = new HashMap<>();

    /**
     * redis中秒杀key前缀
     */
    private static final String SEC_KILL_KEY_PREFIX = "secKillGoods:";

    /**
     * redis中商品订单key前缀
     */
    public static final String SEC_KILL_GOODS_KEY_PREFIX = "secKillOrder:";

    /**
     * redis中url code前缀
     */
    private static final String SEC_KILL_URL_CODE_PREFIX = "secKillUrlCode:";
    /**
     * Mysql版本 qps 1101
     * @param goodsId
     */
    //@Override
    //@Transactional(rollbackFor = Exception.class)
    //public void secKill(Long goodsId) {
    //    // 判断商品合法性 goodsId
    //    final SecurityUser principal = (SecurityUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
    //    //final SecKillGoods goods = secKillGoodsRepository.findById(goodsId).get();
    //    //if (Objects.nonNull(goods) && goods.getStockCount() > 0 && Instant.now().isAfter(goods.getStartDate()) && Instant.now().isBefore(goods.getEndDate())) {
    //        //goods.setStockCount(goods.getStockCount()-1);
    //        //secKillGoodsRepository.save(goods);
    //        if (secKillGoodsRepository.secKill(goodsId) > 0) {
    //            // 库存扣减成功 则处理订单
    //            final Goods goods1 = goodsRepository.findById(goodsId).get();
    //            final Order order = orderRepository.save(new Order().setUserId(principal.getId()).setGoodsId(goodsId).setGoodsName(goods1.getGoodsName()).setGoodsCount(1).setGoodsPrice(goods1.getGoodsPrice())
    //                    .setCreateDate(Instant.now()).setStatus(0));
    //            // 创建订单
    //            final SecKillOrder secKillOrder = new SecKillOrder().setUserId(principal.getId()).setGoodsId(goodsId).setOrderId(order.getId());
    //            secKillOrderRepository.save(secKillOrder);
    //        }
    //
    //    //}
    //}

    /**
     * Redis版本 preload
     * qps 1224
     * @param goodsId
     */
    //@Override
    //@Transactional(rollbackFor = Exception.class)
    //public ResultMsg secKill(Long goodsId) {
    //    if (GOODS_CAN_BY_MAP.getOrDefault(goodsId, true)) {
    //        return new ResultMsg(500, "ERROR", "秒杀失败！无库存");
    //    }
    //    final SecurityUser principal = (SecurityUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
    //    if (redisTemplate.opsForValue().decrement("secKillGoods:"+goodsId) >= 0) {
    //        // 库存扣减成功 则处理订单
    //        final Goods goods1 = goodsRepository.findById(goodsId).get();
    //        final Order order = orderRepository.save(new Order().setUserId(principal.getId()).setGoodsId(goodsId).setGoodsName(goods1.getGoodsName()).setGoodsCount(1).setGoodsPrice(goods1.getGoodsPrice())
    //                .setCreateDate(Instant.now()).setStatus(0));
    //        // 创建订单
    //        final SecKillOrder secKillOrder = new SecKillOrder().setUserId(principal.getId()).setGoodsId(goodsId).setOrderId(order.getId());
    //        secKillOrderRepository.save(secKillOrder);
    //        return new ResultMsg(200, "SUCCESS", "秒杀成功");
    //
    //    } else {
    //        GOODS_CAN_BY_MAP.put(goodsId, true);
    //        redisTemplate.opsForValue().increment("secKillGoods:"+goodsId);
    //        return new ResultMsg(500, "ERROR", "秒杀失败！无库存");
    //
    //    }
    //
    //}

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void makeOrder(SecKillMessage secKillMessage) {
        final Long userId = secKillMessage.getUserId();
        final Long goodsId = secKillMessage.getGoodsId();
        // 从redis获取库存 更新数据库 PS：这里不急要每次都更新数据库，可以选择一个时间点将redis的库存一次同步到mysql中
        final Integer stockCount = (Integer) redisTemplate.opsForValue().get(SEC_KILL_KEY_PREFIX + goodsId);
        log.info("redis中的库存数={}", stockCount);
        secKillGoodsRepository.updateStockCount(goodsId, stockCount);
        final Goods goods1 = goodsRepository.findById(goodsId).get();
        final Order order = orderRepository.save(new Order().setUserId(userId).setGoodsId(goodsId).setGoodsName(goods1.getGoodsName()).setGoodsCount(1).setGoodsPrice(goods1.getGoodsPrice())
                .setCreateDate(Instant.now()).setStatus(OrderStatusEnum.PENDING_PAY.getCode()));
        // 创建订单
        final SecKillOrder secKillOrder = new SecKillOrder().setUserId(userId).setGoodsId(goodsId).setOrderId(order.getId());
        secKillOrderRepository.save(secKillOrder);
        // 下单后的记录在redis中， 供客户端轮训查询结果 减少数据库压力和用户超买判断（这里每个用户限购一件，如果可以购买多件的话需要维护一个下单次数）
        String key = "userId=" + userId + "goodsId=" + goodsId;
        redisTemplate.opsForValue().set(SEC_KILL_GOODS_KEY_PREFIX+key, OrderStatusEnum.PENDING_PAY.getCode());
    }

    /**
     * Redis版本 preload
     * qps 1308
     * @param goodsId
     */
    @Override
    public ResultMsg secKill(Long goodsId) {
        // 库存售罄内存标识
        if (GOODS_CAN_BY_MAP.getOrDefault(goodsId, true)) {
            return new ResultMsg(ResponseCodeEnum.NO_STOCK.getCode(), ResponseCodeEnum.NO_STOCK.getDesc(), null);
        }
        final ValueOperations valueOperations = redisTemplate.opsForValue();
        final SecurityUser principal = (SecurityUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
        String userGoodKey =  "userId=" + principal.getId() + "goodsId=" + goodsId;
        // 判断用户多买问题
        if (valueOperations.getOperations().hasKey(SEC_KILL_GOODS_KEY_PREFIX + userGoodKey)) {
            return new ResultMsg(ResponseCodeEnum.SEC_KILL_LIMIT.getCode(), ResponseCodeEnum.SEC_KILL_LIMIT.getDesc(), null);
        }
        // 从缓存拿到商品信息
        final String key = SEC_KILL_KEY_PREFIX + goodsId;
        if (!valueOperations.getOperations().hasKey(key)) {
            // 从数据库load到缓存中 这里需要获取锁
            final String LOCK = SEC_KILL_KEY_PREFIX+"lock";
            // 当业务执行时间超锁所设置的有效期 防止锁被别人释放
            final String VALUE = UUID.randomUUID().toString();
            final Integer LOCK_EXPIRE_TIME = 5;
            if (valueOperations.setIfAbsent(LOCK, VALUE, LOCK_EXPIRE_TIME, TimeUnit.SECONDS)) {
                // 获取到锁 从数据库load数据到缓存
                final SecKillGoods secKillGoods = secKillGoodsRepository.findById(goodsId).get();
                if (Objects.nonNull(secKillGoods)) {
                    valueOperations.set(key, secKillGoods.getStockCount());
                } else {
                    // 对于不合法的商品数据 防止缓存穿透（数据库和缓存都没有） 设置过期时间
                    valueOperations.set(key, 0, 5, TimeUnit.SECONDS);
                }
                // 释放锁 使用lua脚本来保证原子性
                if (((Boolean) redisTemplate.execute(unlockScript, Collections.singletonList(LOCK), VALUE))) {
                    log.info("释放锁成功");
                } else {
                    log.info("释放锁失败");
                }
            } else {
                log.info("获取锁失败");
                return new ResultMsg(ResponseCodeEnum.FAIL.getCode(), ResponseCodeEnum.FAIL.getDesc(), null);
            }
        }
        if (valueOperations.decrement(key) >= 0) {
            // 下单操作放到rabbitmq中 削峰 解耦
            rabbitMqSecKillSender.sendForSecKillOrder(new SecKillMessage(principal.getId(), goodsId));
            return new ResultMsg(ResponseCodeEnum.WAITING.getCode(), ResponseCodeEnum.WAITING.getDesc(), OrderStatusEnum.PENDING_PROCESS.getCode());

        } else {
            GOODS_CAN_BY_MAP.put(goodsId, true);
            // 防止库存数量为负数
            valueOperations.increment(key);
            return new ResultMsg(ResponseCodeEnum.NO_STOCK.getCode(), ResponseCodeEnum.NO_STOCK.getDesc(), null);
        }

    }

    /**
     * 缓存预热
     */
    @Override
    public void afterPropertiesSet() {
        secKillGoodsRepository.findAll().forEach(secKillGoods ->
                {
                    redisTemplate.opsForValue()
                            .set(SEC_KILL_KEY_PREFIX+secKillGoods.getGoodsId(),
                                    secKillGoods.getStockCount()
                            );
                    // false即库存不为空
                    if (secKillGoods.getStockCount() > 0) {
                        GOODS_CAN_BY_MAP.put(secKillGoods.getGoodsId(), false);
                    } else {
                        GOODS_CAN_BY_MAP.put(secKillGoods.getGoodsId(), true);
                    }
                }

        );
    }


    @Override
    public Integer getOrderStatus(Long goodsId) {
        final SecurityUser principal = (SecurityUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
        String key = "userId=" + principal.getId() + "goodsId=" + goodsId;
        if (redisTemplate.opsForValue().getOperations().hasKey(SEC_KILL_GOODS_KEY_PREFIX + key)) {
            return ((Integer) redisTemplate.opsForValue().get(SEC_KILL_GOODS_KEY_PREFIX + key));
        }
        return OrderStatusEnum.PENDING_PROCESS.getCode();
    }

    @Override
    public String generateSecKillCode(Long goodsId) {
        final SecurityUser principal = (SecurityUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
        String key = SEC_KILL_URL_CODE_PREFIX + "userId=" + principal.getId() + "goodsId=" + goodsId;
        final String code = UUID.randomUUID().toString();
        redisTemplate.opsForValue().set(key, code, 10, TimeUnit.MINUTES);
        return code;
    }

    @Override
    public Boolean checkUrlInvalid(String code, Long goodsId) {
        final SecurityUser principal = (SecurityUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
        String key = SEC_KILL_URL_CODE_PREFIX + "userId=" + principal.getId() + "goodsId=" + goodsId;
        final String s = (String) redisTemplate.opsForValue().get(key);
        return code.equals(s);
    }
}
